﻿using System;
using System.Runtime.InteropServices;
using MatlabBridge.NativeMethods;

namespace MatlabBridge
{
    public class MxArray : IDisposable
    {
        #region Create methods

        public static MxArray CreateFromBoolean(bool value)
        {
            IntPtr pm = NativeMethods.libmx.mxCreateLogicalScalar(value);
            return MxArray.FromPointer(pm);
        }
        public static MxArray CreateFromScalar(double value)
        {
            IntPtr pm = NativeMethods.libmx.mxCreateDoubleScalar(value);
            return MxArray.FromPointer(pm);
        }
        public static MxArray CreateFromString(string value)
        {
            IntPtr pm = NativeMethods.libmx.mxCreateString(value);
            return MxArray.FromPointer(pm);
        }

        public static MxArray CreateMatrix(params int[] size)
        {
            return MxArray.CreateMatrix(typeof(double), false, size);
        }
        public static MxArray CreateMatrix(bool complex, params int[] size)
        {
            return MxArray.CreateMatrix(typeof(double), complex, size);
        }
        public static MxArray CreateMatrix(Type t, params int[] size)
        {
            return MxArray.CreateMatrix(t, false, size);
        }
        public static MxArray CreateMatrix(Type t, bool complex, params int[] size)
        {
            var classid = NativeMethods.Utils.ClassIDFromType(t);
            var complexflag = complex ? mxComplexity.mxCOMPLEX : mxComplexity.mxREAL;
            IntPtr pm = MatlabException.ThrowException("Unable to create matrix.",
                NativeMethods.libmx.mxCreateNumericArray(new mwSize(size.Length), size, classid, complexflag));
            return MxArray.FromPointer(pm);
        }

        #endregion

        #region Core

        protected MxArray() { }
        internal MxArray(MxArray array)
        {
            this.pm = array.pm;
        }
        public static MxArray FromPointer(IntPtr pm)
        {
            if (pm == IntPtr.Zero)
                throw new ArgumentOutOfRangeException("pm", "The unmanaged mxArray pointer was null.");

            MxArray ar = new MxArray();
            ar.pm = pm;
            return ar;
        }

        ~MxArray()
        {
            System.Diagnostics.Debug.WriteLine("Undisposed MxArray was finalized. (pm = " + pm.ToString() + ")");
            this.Dispose(false);
        }
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
            }

            if (pm != IntPtr.Zero)
            {
                libmx.mxDestroyArray(pm);
                pm = IntPtr.Zero;
            }
        }

        protected IntPtr pm;
        public IntPtr UnmanagedPointer
        {
            get { return pm; }
        }
        
        #endregion

        protected T SupersededBy<T>(T newobj) where T : MxArray
        {
            pm = IntPtr.Zero;
            GC.SuppressFinalize(this);

            return newobj;
        }
        public MxArrayCell AsCellArray()
        {
            if (!this.IsCellArray)
                throw new InvalidCastException("The MxArray is not a cell array.");

            return SupersededBy(new MxArrayCell(this));
        }
        public MxArrayStruct AsStructArray()
        {
            if (!this.IsStructArray)
                throw new InvalidCastException("The MxArray is not a structure.");

            return SupersededBy(new MxArrayStruct(this));
        }

        public double AsScalar()
        {
            return libmx.mxGetScalar(pm);
        }
        public string AsString()
        {
            int strlen = this.ElementCount + 1;
            IntPtr pstr = Marshal.AllocHGlobal(strlen);
            NativeMethods.libmx.mxGetString(pm, pstr, new mwSize(strlen));
            string str = Marshal.PtrToStringAnsi(pstr);
            Marshal.FreeHGlobal(pstr);

            return str;
        }
        
        public void FillVectorDouble(double[] array)
        {
            if (typeof(double) != this.ClassType)
                throw new InvalidOperationException("The type of the underlying MxArray must match the CLR array type. To cast the result, you must perform the cast after the data has been copied.");
            if (array.Length < ElementCount)
                throw new ArgumentOutOfRangeException("The array is not large enough to hold the data.");

            Marshal.Copy(this.GetDataPointer(), array, 0, this.ElementCount);
        }
        public double[] AsVectorDouble()
        {
            double[] array = new double[this.ElementCount];
            FillVectorDouble(array);
            return array;
        }

        public void SetDataVector(double[] array)
        {
            if (typeof(double) != this.ClassType)
                throw new InvalidOperationException("The type of the underlying MxArray must match the CLR array type.");
            if (array.Length > ElementCount)
                throw new ArgumentOutOfRangeException("The array is not large enough to hold the data.");

            Marshal.Copy(array, 0, this.GetDataPointerReal(), array.Length);
        }

        public int[] GetSize()
        {
            IntPtr p = NativeMethods.libmx.mxGetDimensions(pm);
            int dims = this.DimensionCount;
            int[] size = new int[dims];
            for (int i = 0; i < dims; i++)
            {
                size[i] = Marshal.ReadInt32(p, i * 4);
            }
            return size;
        }
        public IntPtr GetDataPointer()
        {
            return NativeMethods.libmx.mxGetData(pm);
        }
        public IntPtr GetDataPointerReal()
        {
            return NativeMethods.libmx.mxGetPr(pm);
        }
        public IntPtr GetDataPointerImaginary()
        {
            return NativeMethods.libmx.mxGetPi(pm);
        }

        public Type ClassType
        {
            get { return NativeMethods.Utils.TypeFromClassID(NativeMethods.libmx.mxGetClassID(pm)); }
        }
        public string ClassName
        {
            get { return NativeMethods.libmx.mxGetClassName(pm); }
        }
        
        public int DimensionCount
        {
            get { return libmx.mxGetNumberOfDimensions(pm).value; }
        }
        public int ElementCount
        {
            get { return libmx.mxGetNumberOfElements(pm).value; }
        }
        public int RowCount
        {
            get { return NativeMethods.libmx.mxGetM(pm).value; }
        }
        public int ColumnCount
        {
            get { return NativeMethods.libmx.mxGetN(pm).value; }
        }
        
        #region Is

        public bool IsClass(string classname) { return libmx.mxIsClass(pm, classname); }
        public bool IsCellArray { get { return libmx.mxIsCell(pm); } }
        public bool IsStructArray { get { return libmx.mxIsStruct(pm); } }
        public bool IsComplex { get { return libmx.mxIsComplex(pm); } }
        public bool IsNumeric { get { return libmx.mxIsNumeric(pm); } }

        public bool IsEmpty { get { return libmx.mxIsEmpty(pm); } }
        public bool IsSparse { get { return libmx.mxIsSparse(pm); } }

        public bool IsFromGlobalWorkspace { get { return libmx.mxIsFromGlobalWS(pm); } }
        public bool IsLogicalScalar { get { return libmx.mxIsLogicalScalar(pm); } }
        public bool IsLogicalScalarTrue { get { return libmx.mxIsLogicalScalarTrue(pm); } }

        public bool IsChar { get { return libmx.mxIsChar(pm); } }
        public bool IsDouble { get { return libmx.mxIsDouble(pm); } }
        public bool IsInt16 { get { return libmx.mxIsInt16(pm); } }
        public bool IsInt32 { get { return libmx.mxIsInt32(pm); } }
        public bool IsInt64 { get { return libmx.mxIsInt64(pm); } }
        public bool IsInt8 { get { return libmx.mxIsInt8(pm); } }
        public bool IsLogical { get { return libmx.mxIsLogical(pm); } }
        public bool IsSingle { get { return libmx.mxIsSingle(pm); } }
        public bool IsUInt16 { get { return libmx.mxIsUint16(pm); } }
        public bool IsUInt32 { get { return libmx.mxIsUint32(pm); } }
        public bool IsUInt64 { get { return libmx.mxIsUint64(pm); } }
        public bool IsUInt8 { get { return libmx.mxIsUint8(pm); } }
        
        #endregion
    }

    public class MxArrayCell : MxArray
    {
        internal MxArrayCell(MxArray array) : base(array) { }

        public MxArray this[int idx]
        {
            get { return MxArray.FromPointer(NativeMethods.libmx.mxGetCell(pm, new mwIndex(idx))); }
        }
    }
    public class MxArrayStruct : MxArray
    {
        internal MxArrayStruct(MxArray array) : base(array) { }

        public int FieldCount
        {
            get
            {
                return MatlabException.ThrowException("The MxArray is not a structure.",
                    libmx.mxGetNumberOfFields(pm));
            }
        }

        public MxArray this[string fieldname]
        {
            get
            {
                return MxArray.FromPointer(libmx.mxGetField(pm, new mwIndex(0), fieldname));
            }
        }
        public MxArray this[string fieldname, int idx]
        {
            get
            {
                return MxArray.FromPointer(libmx.mxGetField(pm, new mwIndex(idx), fieldname));
            }
        }
    }
}
